/**
 * Copyright (C) 2023 maminjie <canpool@163.com>
 * Copyright (C) 2022 Syarif Fakhri
 * Copyright (C) 2017 Uwe Kindler
 * SPDX-License-Identifier: LGPL-2.1
 **/

#include "QxDockAutoHideContainer.h"
#include "QxDockAutoHideSideBar.h"
#include "QxDockAutoHideTab.h"
#include "QxDockAreaWidget.h"
#include "QxDockFactory.h"
#include "QxDockManager.h"
#include "QxDockResizeHandle.h"

#include <QApplication>
#include <QBoxLayout>
#include <QCursor>
#include <QPainter>
#include <QPointer>
#include <QSplitter>
#include <QXmlStreamWriter>

#include <iostream>

QX_BEGIN_NAMESPACE

static const int ResizeMargin = 30;

bool static isHorizontalArea(SideBarLocation Area)
{
    switch (Area) {
    case SideBarLocation::SideBarTop:
    case SideBarLocation::SideBarBottom:
        return true;
    case SideBarLocation::SideBarLeft:
    case SideBarLocation::SideBarRight:
        return false;
    default:
        return true;
    }

    return true;
}

Qt::Edge static edgeFromSideTabBarArea(SideBarLocation Area)
{
    switch (Area) {
    case SideBarLocation::SideBarTop:
        return Qt::BottomEdge;
    case SideBarLocation::SideBarBottom:
        return Qt::TopEdge;
    case SideBarLocation::SideBarLeft:
        return Qt::RightEdge;
    case SideBarLocation::SideBarRight:
        return Qt::LeftEdge;
    default:
        return Qt::LeftEdge;
    }

    return Qt::LeftEdge;
}

int resizeHandleLayoutPosition(SideBarLocation Area)
{
    switch (Area) {
    case SideBarLocation::SideBarBottom:
    case SideBarLocation::SideBarRight:
        return 0;

    case SideBarLocation::SideBarTop:
    case SideBarLocation::SideBarLeft:
        return 1;

    default:
        return 0;
    }

    return 0;
}

/**
 * Private data of DockAutoHideContainer - pimpl
 */
struct DockAutoHideContainerPrivate {
    DockAutoHideContainer *_this;
    DockAreaWidget *dockArea{nullptr};
    DockWidget *dockWidget{nullptr};
    SideBarLocation SideTabBarArea = SideBarNone;
    QBoxLayout *Layout = nullptr;
    DockResizeHandle *ResizeHandle = nullptr;
    QSize Size;   // creates invalid size
    QPointer<DockAutoHideTab> SideTab;

    /**
     * Private data constructor
     */
    DockAutoHideContainerPrivate(DockAutoHideContainer *_public);

    /**
     * Convenience function to get a dock widget area
     */
    DockWidgetArea getDockWidgetArea(SideBarLocation area)
    {
        switch (area) {
        case SideBarLocation::SideBarLeft:
            return LeftDockWidgetArea;
        case SideBarLocation::SideBarRight:
            return RightDockWidgetArea;
        case SideBarLocation::SideBarBottom:
            return BottomDockWidgetArea;
        case SideBarLocation::SideBarTop:
            return TopDockWidgetArea;
        default:
            return LeftDockWidgetArea;
        }

        return LeftDockWidgetArea;
    }

    /**
     * Update the resize limit of the resize handle
     */
    void updateResizeHandleSizeLimitMax()
    {
        auto Rect = _this->dockContainer()->contentRect();
        const auto maxResizeHandleSize = ResizeHandle->orientation() == Qt::Horizontal ? Rect.width() : Rect.height();
        ResizeHandle->setMaxResizeSize(maxResizeHandleSize - ResizeMargin);
    }

    /**
     * Convenience function to check, if this is an horizontal area
     */
    bool isHorizontal() const
    {
        return isHorizontalArea(SideTabBarArea);
    }

    /**
     * Forward this event to the dock container
     */
    void forwardEventToDockContainer(QEvent *event)
    {
        auto DockContainer = _this->dockContainer();
        if (DockContainer) {
            DockContainer->handleAutoHideWidgetEvent(event, _this);
        }
    }

};   // struct DockAutoHideContainerPrivate

DockAutoHideContainerPrivate::DockAutoHideContainerPrivate(DockAutoHideContainer *_public) : _this(_public)
{
}

DockContainerWidget *DockAutoHideContainer::dockContainer() const
{
    return internal::findParent<DockContainerWidget *>(this);
}

DockAutoHideContainer::DockAutoHideContainer(DockWidget *dockWidget, SideBarLocation area,
                                               DockContainerWidget *parent)
    : Super(parent), d(new DockAutoHideContainerPrivate(this))
{
    hide();   // auto hide dock container is initially always hidden
    d->SideTabBarArea = area;
    d->SideTab = componentsFactory()->createDockWidgetSideTab(nullptr);
    connect(d->SideTab, &DockAutoHideTab::pressed, this, &DockAutoHideContainer::toggleCollapseState);
    d->dockArea = new DockAreaWidget(dockWidget->dockManager(), parent);
    d->dockArea->setObjectName("autoHideDockArea");
    d->dockArea->setAutoHideDockContainer(this);

    setObjectName("autoHideDockContainer");

    d->Layout = new QBoxLayout(isHorizontalArea(area) ? QBoxLayout::TopToBottom : QBoxLayout::LeftToRight);
    d->Layout->setContentsMargins(0, 0, 0, 0);
    d->Layout->setSpacing(0);
    setLayout(d->Layout);
    d->ResizeHandle = new DockResizeHandle(edgeFromSideTabBarArea(area), this);
    d->ResizeHandle->setMinResizeSize(64);
    bool OpaqueResize = DockManager::testConfigFlag(DockManager::OpaqueSplitterResize);
    d->ResizeHandle->setOpaqueResize(OpaqueResize);
    d->Size = d->dockArea->size();

    addDockWidget(dockWidget);
    parent->registerAutoHideWidget(this);
    // The dock area should not be added to the layout before it contains the
    // dock widget. If you add it to the layout before it contains the dock widget
    // then you will likely see this warning for OpenGL widgets or QAxWidgets:
    // setGeometry: Unable to set geometry XxY+Width+Height on QWidgetWindow/'WidgetClassWindow
    d->Layout->addWidget(d->dockArea);
    d->Layout->insertWidget(resizeHandleLayoutPosition(area), d->ResizeHandle);
}

void DockAutoHideContainer::updateSize()
{
    auto dockContainerParent = dockContainer();
    if (!dockContainerParent) {
        return;
    }

    auto rect = dockContainerParent->contentRect();

    switch (sideBarLocation()) {
    case SideBarLocation::SideBarTop:
        resize(rect.width(), qMin(rect.height() - ResizeMargin, d->Size.height()));
        move(rect.topLeft());
        break;

    case SideBarLocation::SideBarLeft:
        resize(qMin(d->Size.width(), rect.width() - ResizeMargin), rect.height());
        move(rect.topLeft());
        break;

    case SideBarLocation::SideBarRight: {
        resize(qMin(d->Size.width(), rect.width() - ResizeMargin), rect.height());
        QPoint p = rect.topRight();
        p.rx() -= (width() - 1);
        move(p);
    } break;

    case SideBarLocation::SideBarBottom: {
        resize(rect.width(), qMin(rect.height() - ResizeMargin, d->Size.height()));
        QPoint p = rect.bottomLeft();
        p.ry() -= (height() - 1);
        move(p);
    } break;

    default:
        break;
    }
}

DockAutoHideContainer::~DockAutoHideContainer()
{
    QX_DOCK_PRINT("~DockAutoHideContainer");

    // Remove event filter in case there are any queued messages
    qApp->removeEventFilter(this);
    if (dockContainer()) {
        dockContainer()->removeAutoHideWidget(this);
    }

    if (d->SideTab) {
        delete d->SideTab;
    }

    delete d;
}

DockAutoHideSideBar *DockAutoHideContainer::sideBar() const
{
    if (d->SideTab) {
        return d->SideTab->sideBar();
    } else {
        auto DockContainer = dockContainer();
        return DockContainer ? DockContainer->sideTabBar(d->SideTabBarArea) : nullptr;
    }
}

DockAutoHideTab *DockAutoHideContainer::autoHideTab() const
{
    return d->SideTab;
}

DockWidget *DockAutoHideContainer::dockWidget() const
{
    return d->dockWidget;
}

void DockAutoHideContainer::addDockWidget(DockWidget *dockWidget)
{
    if (d->dockWidget) {
        // Remove the old dock widget at this area
        d->dockArea->removeDockWidget(d->dockWidget);
    }

    d->dockWidget = dockWidget;
    d->SideTab->setDockWidget(dockWidget);
    DockAreaWidget *OldDockArea = dockWidget->dockAreaWidget();
    auto IsRestoringState = dockWidget->dockManager()->isRestoringState();
    if (OldDockArea && !IsRestoringState) {
        // The initial size should be a little bit bigger than the original dock
        // area size to prevent that the resize handle of this auto hid dock area
        // is near of the splitter of the old dock area.
        d->Size = OldDockArea->size() + QSize(16, 16);
        OldDockArea->removeDockWidget(dockWidget);
    }
    d->dockArea->addDockWidget(dockWidget);
    updateSize();
}

SideBarLocation DockAutoHideContainer::sideBarLocation() const
{
    return d->SideTabBarArea;
}

void DockAutoHideContainer::setSideBarLocation(SideBarLocation SideBarLocation)
{
    if (d->SideTabBarArea == SideBarLocation) {
        return;
    }

    d->SideTabBarArea = SideBarLocation;
    d->Layout->removeWidget(d->ResizeHandle);
    d->Layout->setDirection(isHorizontalArea(SideBarLocation) ? QBoxLayout::TopToBottom : QBoxLayout::LeftToRight);
    d->Layout->insertWidget(resizeHandleLayoutPosition(SideBarLocation), d->ResizeHandle);
    d->ResizeHandle->setHandlePosition(edgeFromSideTabBarArea(SideBarLocation));
    internal::repolishStyle(this, internal::RepolishDirectChildren);
}

DockAreaWidget *DockAutoHideContainer::dockAreaWidget() const
{
    return d->dockArea;
}

void DockAutoHideContainer::moveContentsToParent()
{
    cleanupAndDelete();
    // If we unpin the auto hide dock widget, then we insert it into the same
    // location like it had as a auto hide widget.  This brings the least surprise
    // to the user and he does not have to search where the widget was inserted.
    d->dockWidget->setDockArea(nullptr);
    auto DockContainer = dockContainer();
    DockContainer->addDockWidget(d->getDockWidgetArea(d->SideTabBarArea), d->dockWidget);
}

void DockAutoHideContainer::cleanupAndDelete()
{
    const auto dockWidget = d->dockWidget;
    if (dockWidget) {
        auto SideTab = d->SideTab;
        SideTab->removeFromSideBar();
        SideTab->setParent(nullptr);
        SideTab->hide();
    }

    hide();
    deleteLater();
}

void DockAutoHideContainer::saveState(QXmlStreamWriter &s)
{
    s.writeStartElement("Widget");
    s.writeAttribute("Name", d->dockWidget->objectName());
    s.writeAttribute("Closed", QString::number(d->dockWidget->isClosed() ? 1 : 0));
    s.writeAttribute("Size", QString::number(d->isHorizontal() ? d->Size.height() : d->Size.width()));
    s.writeEndElement();
}

void DockAutoHideContainer::toggleView(bool Enable)
{
    if (Enable) {
        if (d->SideTab) {
            d->SideTab->show();
        }
    } else {
        if (d->SideTab) {
            d->SideTab->hide();
        }
        hide();
        qApp->removeEventFilter(this);
    }
}

void DockAutoHideContainer::collapseView(bool Enable)
{
    if (Enable) {
        hide();
        qApp->removeEventFilter(this);
    } else {
        updateSize();
        d->updateResizeHandleSizeLimitMax();
        raise();
        show();
        d->dockWidget->dockManager()->setDockWidgetFocused(d->dockWidget);
        qApp->installEventFilter(this);
    }

    QX_DOCK_PRINT("DockAutoHideContainer::collapseView " << Enable);
    d->SideTab->updateStyle();
}

void DockAutoHideContainer::toggleCollapseState()
{
    collapseView(isVisible());
}

void DockAutoHideContainer::setSize(int Size)
{
    if (d->isHorizontal()) {
        d->Size.setHeight(Size);
    } else {
        d->Size.setWidth(Size);
    }

    updateSize();
}

/**
 * Returns true if the object given in ancestor is an ancestor of the object
 * given in descendant
 */
static bool objectIsAncestorOf(const QObject *descendant, const QObject *ancestor)
{
    if (!ancestor) {
        return false;
    }
    while (descendant) {
        if (descendant == ancestor) {
            return true;
        }
        descendant = descendant->parent();
    }
    return false;
}

/**
 * Returns true if the object given in ancestor is the object given in descendant
 * or if it is an ancestor of the object given in descendant
 */
static bool isObjectOrAncestor(const QObject *descendant, const QObject *ancestor)
{
    if (ancestor && (descendant == ancestor)) {
        return true;
    } else {
        return objectIsAncestorOf(descendant, ancestor);
    }
}

bool DockAutoHideContainer::eventFilter(QObject *watched, QEvent *event)
{
    // A switch case statement would be nicer here, but we cannot use
    // internal::FloatingWidgetDragStartEvent in a switch case
    if (event->type() == QEvent::Resize) {
        if (!d->ResizeHandle->isResizing()) {
            updateSize();
        }
    } else if (event->type() == QEvent::MouseButtonPress) {
        auto widget = qobject_cast<QWidget *>(watched);
        // Ignore non widget events
        if (!widget) {
            return Super::eventFilter(watched, event);
        }

        // Now check, if the user clicked into the side tab and ignore this event,
        // because the side tab click handler will call collapseView(). If we
        // do not ignore this here, then we will collapse the container and the side tab
        // click handler will uncollapse it
        if (widget == d->SideTab.data()) {
            return Super::eventFilter(watched, event);
        }

        // Now we check, if the user clicked inside of this auto hide container.
        // If the click is inside of this auto hide container, then we can
        // ignore the event, because the auto hide overlay should not get collapsed if
        // user works in it
        if (isObjectOrAncestor(widget, this)) {
            return Super::eventFilter(watched, event);
        }

        // Ignore the mouse click if it is not inside of this container
        if (!isObjectOrAncestor(widget, dockContainer())) {
            return Super::eventFilter(watched, event);
        }

        // user clicked into container - collapse the auto hide widget
        collapseView(true);
    } else if (event->type() == internal::FloatingWidgetDragStartEvent) {
        // If we are dragging our own floating widget, the we do not need to
        // collapse the view
        auto FloatingWidget = dockContainer()->floatingWidget();
        if (FloatingWidget != watched) {
            collapseView(true);
        }
    } else if (event->type() == internal::DockedWidgetDragStartEvent) {
        collapseView(true);
    }

    return Super::eventFilter(watched, event);
}

void DockAutoHideContainer::resizeEvent(QResizeEvent *event)
{
    Super::resizeEvent(event);
    if (d->ResizeHandle->isResizing()) {
        d->Size = this->size();
        d->updateResizeHandleSizeLimitMax();
    }
}

void DockAutoHideContainer::leaveEvent(QEvent *event)
{
    // Resizing of the dock container via the resize handle in non opaque mode
    // mays cause a leave event that is not really a leave event. Therefore
    // we check here, if we are really outside of our rect.
    auto pos = mapFromGlobal(QCursor::pos());
    if (!rect().contains(pos)) {
        d->forwardEventToDockContainer(event);
    }
    Super::leaveEvent(event);
}

bool DockAutoHideContainer::event(QEvent *event)
{
    switch (event->type()) {
    case QEvent::Enter:
    case QEvent::Hide:
        d->forwardEventToDockContainer(event);
        break;

    case QEvent::MouseButtonPress:
        return true;
        break;

    default:
        break;
    }

    return Super::event(event);
}

QX_END_NAMESPACE
